package main

import (
	"fmt"
	"unicode/utf8"
)

/*
可以使用两个视角来看待 Go 字符串的组成，一种是字节视角。
Go 字符串是由一个可空的字节序列组成，字节的个数称为字符串的长度；
另外一种是字符视角。Go 字符串是由一个可空的字符序列构成。Go 字符串中的每个字符都是一个 Unicode 字符。
*/

func main() {
	//字符和字节
	string1()

	//字符字面值
	stringChar()

	//字符串遍历(迭代)
	stringFor()

	//字符串拼接
	stringJoin()

	//字符串比较
	stringCompare()

	//字符串转换
	stringTransfer()
}

func string1() {
	//由“中国人”构成的字符串的字节序列长度为 9。并且，仅从某一个输出的字节来看，它是不能与字符串中的任一个字符对应起来的。
	var s = "中国人"
	fmt.Printf("the length of s = %d\n", len(s)) // 9
	for i := 0; i < len(s); i++ {
		fmt.Printf("0x%x ", s[i]) // 0xe4 0xb8 0xad 0xe5 0x9b 0xbd 0xe4 0xba 0xba
	}
	fmt.Printf("\n")

	//在这段代码中，我们输出了字符串中的字符数量，也输出了这个字符串中的每个字符。
	var s2 = "中国人"
	fmt.Println("the character count in s is", utf8.RuneCountInString(s2)) // 3
	for _, c := range s2 {
		fmt.Printf("0x%x ", c) // 0x4e2d 0x56fd 0x4eba
	}
	fmt.Printf("\n")

	//Go 使用 rune 这个类型来表示一个 Unicode 码点。
	//一个 rune 实例就是一个 Unicode 字符，一个 Go 字符串也可以被视为 rune 实例的集合。
	// 我们可以通过字符字面值来初始化一个 rune 变量。
}

func stringChar() {
	//在 Go 中，字符字面值有多种表示法，最常见的是通过单引号括起的字符字面值，
	a1 := 'a'  // ASCII字符
	a2 := '中'  // Unicode字符集中的中文字符
	a3 := '\n' // 换行字符
	a4 := '\'' // 单引号字符
	fmt.Println(a1, a2, a3, a4)

	//还可以使用 Unicode 专用的转义字符\u 或\U 作为前缀，来表示一个 Unicode 字符，比如：
	//\u 后面接两个十六进制数。如果是用两个十六进制数无法表示的Unicode 字符，我们可以使用\U，\U 后面可以接四个十六进制数来表示一个 Unicode 字符。
	b1 := '\u4e2d'     // 字符：中
	b2 := '\U00004e2d' // 字符：中
	b3 := '\u0027'     // 单引号字符
	fmt.Println(b1, b2, b3)

	//由于表示码点的 rune 本质上就是一个整型数，所以我们还可用整型值来直接作为字符字面值给 rune 变量赋值
	c1 := '\x27' // 使用十六进制表示的单引号字符
	c2 := '\047' // 使用八进制表示的单引号字符
	fmt.Println(c1, c2)

	//字符串是多个字符，所以我们需要把表示单个字符的单引号，换为表示多个字符组成的字符串的双引号就可以了
	d1 := "abc\n"
	d2 := "中国人"
	d3 := "\u4e2d\u56fd\u4eba"                   // 中国人
	d4 := "\U00004e2d\U000056fd\U00004eba"       // 中国人
	d5 := "中\u56fd\u4eba"                        // 中国人，不同字符字面值形式混合在一起
	d6 := "\xe4\xb8\xad\xe5\x9b\xbd\xe4\xba\xba" // 十六进制表示的字符串字面值：中国人 //这个字节序列实际上是“中国人”这个 Unicode 字符串的 UTF-8 编码值
	fmt.Println(d1, d2, d3, d4, d5, d6)

}

func stringFor() {
	//获取长度的时间复杂度是常数时间,因为字符串类型中包含了字符串长度信息，当我们用 len 函数获取字符串长度时，len 函数只要简单地将这个信息提取出来就可以了。
	//我们直接将string 类型通过函数 / 方法参数传入也不会带来太多的开销。因为传入的仅仅是一个“描述符”，而不是真正的字符串数据。

	//--- 下标操作
	//在字符串的实现中，真正存储数据的是底层的数组。字符串的下标操作本质上等价于底层数组的下标操作。
	//通过下标操作，我们获取的是字符串中特定下标上的字节，而不是字符。
	var s = "中国人"
	fmt.Printf("0x%x\n", s[0]) // 0xe4：字符“中” utf-8编码的第一个字节

	//--- 字符迭代
	//Go 有两种迭代形式：常规 for 迭代与 for range 迭代。这两种形式的迭代对字符串进行操作得到的结果是不同的。
	//通过常规 for 迭代对字符串进行的操作是一种字节视角的迭代，每轮迭代得到的的结果都是组成字符串内容的一个字节，以及该字节所在的下标值，这也等价于对字符串底层数组的迭代
	for i := 0; i < len(s); i++ {
		fmt.Printf("index: %d, value: 0x%x\n", i, s[i])
	}
	fmt.Println()
	///输出:
	//index: 0, value: 0xe4
	//index: 1, value: 0xb8
	//index: 2, value: 0xad
	//index: 3, value: 0xe5
	//index: 4, value: 0x9b
	//index: 5, value: 0xbd
	//index: 6, value: 0xe4
	//index: 7, value: 0xba
	//index: 8, value: 0xba

	//通过 for range 迭代，我们每轮迭代得到的是字符串中 Unicode 字符的码点值，以及该字符在字符串中的偏移值
	//可以通过这样的迭代，获取字符串中的字符个数，而通过 Go 提供的内置函数 len，我们只能获取字符串内容的长度（字节个数）。
	for i, v := range s {
		fmt.Printf("index: %d, value: 0x%x\n", i, v)
	}
	fmt.Println()
	///输出:
	//index: 0, value: 0x4e2d
	//index: 3, value: 0x56fd
	//index: 6, value: 0x4eba

	//获取字符串中字符个数更专业的方法，是调用标准库 UTF-8 包中的RuneCountInString 函数
	fmt.Println(utf8.RuneCountInString(s)) //3
	fmt.Println()

}

func stringJoin() {
	//--- 字符串连接
	//Go 原生支持通过 +/+= 操作符进行字符串连接,但连接性能就未必是最快的了。
	// 除了这个方法外，Go 还提供了 strings.Builder、strings.Join、fmt.Sprintf 等函数来进行字符串连接操作
	str := "你好,"
	str = str + "小朋友,"
	str += "这里."
	fmt.Println(str) //你好,小朋友,这里.
}

func stringCompare() {
	//Go 字符串类型支持各种比较关系操作符，包括 = =、!= 、>=、<=、> 和 <。
	//如果两个字符串的长度不相同，那么我们不需要比较具体字符串数据

	//==
	s1 := "世界和平"
	s2 := "世界" + "和平"
	fmt.Println(s1 == s2) //true

	//!=
	s1 = "Go"
	s2 = "C"
	fmt.Println(s1 != s2) //true

	//<and<=
	s1 = "12345"
	s2 = "23456"
	fmt.Println(s1 < s2)  //true
	fmt.Println(s1 <= s2) //true

	//>and>=
	s1 = "12345"
	s2 = "123"
	fmt.Println(s1 > s2)  //true
	fmt.Println(s1 >= s2) //true
}

func stringTransfer() {
	//Go支持字符串与字节切片、字符串与rune切片的双向转换，并且这种转换无需调用任何函数，只需使用显式类型转换就可以了。
	var s string = "中国人"
	//string -> []rune
	rs := []rune(s)
	fmt.Printf("%x\n", rs) //[4e2d 56fd 4eba]

	//string -> []byte
	bs := []byte(s)
	fmt.Printf("%x\n", bs) //e4b8ade59bbde4baba

	//[]rune -> string
	s1 := string(rs)
	fmt.Println(s1) //中国人

	//[]byte -> string
	s2 := string(bs)
	fmt.Println(s2) //中国人

	/*
		这样的转型看似简单，但无论是 string 转切片，还是切片转 string，这类转型背后也是有着一定开销的。
		这些开销的根源就在于 string 是不可变的，运行时要为转换后的类型分配新内存。
	*/

}
